﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using QSharp.Signal.NeuralNetwork.Classic;
using QSharp.Signal.NeuralNetwork.Classic.Perceptrons;

namespace QSecEx
{
    public class StockExpert : IXmlSerializable
    {
        #region Fields

        private readonly Network _neural;

        private readonly IList<double> _actualOutput;
        private readonly int _sectionSize;

        #endregion

        #region Constructors

        /// <summary>
        /// 
        /// </summary>
        /// <param name="numDataPoints"></param>
        /// <param name="numForcasts"></param>
        /// <param name="industryLength"></param>
        /// <param name="indexLength"></param>
        /// <param name="stage">The size of each interval, 0.02 - 2%</param>
        /// <param name="intervals">number of intervals (each side excluding 0)</param>
        /// <remarks>
        ///  Parameters include
        ///   - the stock time series data whose dimensions are specified by <paramref name="numDataPoints"/>
        ///   - industry time series data whose length is specified by <paramref name="industryLength"/>
        ///   - index time series data whose length is specified by <paramref name="indexLength"/>
        ///   - [optional] current P/E
        /// </remarks>
        public StockExpert(IList<int> numDataPoints, IList<int> numForcasts, int industryLength, int indexLength
            , double stage, int intervals)
        {
            Stage = stage;
            Intervals = intervals;
            _sectionSize = intervals * 2 + 1;

            var inputNodeCount = numDataPoints.Sum() + industryLength + indexLength;
            var outputNodeCount = numForcasts.Sum()*_sectionSize;
            // TODO strategy for determining hidden layer size
            var hiddenLayerSize = (inputNodeCount + outputNodeCount)/2;
            var csym = new CenSymSigmoid();
            _neural = new Network(csym, csym, inputNodeCount, hiddenLayerSize, outputNodeCount);

            NumDataPoints = new List<int>();
            var sum = 0;
            foreach (var ndp in numDataPoints)
            {
                sum += ndp;
                NumDataPoints.Add(sum);
            }
            
            NumForcasts = new List<int>();
            sum = 0;
            foreach (var nf in numForcasts)
            {
                sum += nf;
                NumForcasts.Add(sum);
            }

            _actualOutput = new double[outputNodeCount];

            IndustryLength = industryLength;
            IndexLength = indexLength;
        }

        #endregion

        #region Properties

        public IList<int> NumDataPoints { get; private set; }
        public IList<int> NumForcasts { get; private set; }

        public int IndustryLength { get; private set; }
        public int IndexLength { get; private set; }

        public double Stage { get; private set; }
        public int Intervals { get; private set; }

        #endregion

        #region Methods

        #region IXmlSerializable members

        public XmlSchema GetSchema()
        {
            throw new NotSupportedException();
        }

        public void ReadXml(XmlReader reader)
        {
            throw new NotImplementedException();
        }

        public void WriteXml(XmlWriter writer)
        {
            throw new NotImplementedException();
        }

        #endregion

        public void Predict(IList<IList<double>> dataPoints, IList<double> industryData,
            IList<double> indexData)
        {
            for (var level = 0; level < NumDataPoints.Count; level++)
            {
                var b = level > 0 ? NumDataPoints[level - 1] : 0;
                var levelData = dataPoints[level];
                for (var index = 0; index < levelData.Count; index++)
                {
                    _neural.Input[b + index] = levelData[index];
                }
            }

            var b1 = NumDataPoints[NumDataPoints.Count - 1];
            for (var i = 0; i < IndustryLength; i++)
            {
                _neural.Input[b1 + i] = industryData[i];
            }

            var b2 = b1 + IndustryLength;
            for (var i = 0; i < IndexLength; i++)
            {
                _neural.Input[b2 + i] = indexData[i];
            }

            _neural.CalculateOutput();
        }

        public void Train(IList<IList<double>> dataPoints, IList<double> industryData,
            IList<double> indexData, IList<IList<double>> actual)
        {
            Predict(dataPoints, industryData, indexData);
            var invLogStage = 1/Math.Log(Stage);

            for (var level = 0; level < NumForcasts.Count; level++)
            {
                var b = level > 0 ? NumForcasts[level - 1] : 0;
                var levelData = actual[level];
                var curr = GetCurrentInputStockPrice(level);
                var logCurr = Math.Log(curr);

                for (var i = 0; i < levelData.Count; i++)
                {
                    var offset = b + i;
                    var r = (Math.Log(levelData[i])-logCurr)*invLogStage;
                    SetActualRate(offset, r);
                }
            }

            _neural.PerformErrorBackpropagation(_actualOutput);
        }

        public void SetInputStockPrice(int level, int index, double value)
        {
            var b = level > 0 ? NumDataPoints[level - 1] : 0;
            _neural.Input[b + index] = value;
        }

        public double GetInputStockPrice(int level, int index)
        {
            var b = level > 0 ? NumDataPoints[level - 1] : 0;
            return _neural.Input[b + index];
        }

        public double GetCurrentInputStockPrice(int level)
        {
            var b = level > 0 ? NumDataPoints[level - 1] : 0;
            var len = NumDataPoints[level] - b;
            return _neural.Input[b + len - 1];
        }

        private void SetActualRate(int offset, double r)
        {
            var r0 = (int)Math.Floor(r);
            var r1 = r0+1;

            if (r1 > Intervals)
            {
                SetOutput(offset, Intervals, 1);
            }
            else if (r0 < -Intervals)
            {
                SetOutput(offset, -Intervals, 1);
            }
            else
            {
                var d0 = r - r0;
                var d1 = r1 - r;
                SetOutput(offset, r0, d1);
                SetOutput(offset, r1, d0);
            }
        }

        private void SetOutput(int offset, int r, double magnitude)
        {
            var rr = r + Intervals;
            _neural.Output[(offset) * _sectionSize + rr] = magnitude;
        }

        #endregion
    }
}
